package framework.aop.support;

import framework.aop.InchAopConfig;
import framework.aop.aspect.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * a class that analysis and store the point cuts bean info
 * including the matching of pointcut and aspect methods assigned to it
 *
 * Method pointCutMatch() tells if it is a point cut
 */
public class InchAdvisedSupport {
    // the class of the point cut
    private Class targetClass;
    // the instance of the point cut class
    private Object target;
    // the point cuts methods
    private String[] pointCuts;
    // the config class that read info from configuration file
    private InchAopConfig config;

    //store the method and what method need to be execute before/after/... it;
    private transient Map<Method, List<Object>> methodCache;

    /**
     * constructor
     * @param config config class (InchAopConfig)
     */
    public InchAdvisedSupport(InchAopConfig config) {
        this.config = config;
        initAspectMethod();
    }

    /**
     * initial method
     */
    private void initAspectMethod(){
        //get the point cuts
        pointCuts = config.getPointCuts();
        methodCache = new ConcurrentHashMap<Method,List<Object>>();
    }

    /**
     * main class to analysis the config file and try to make
     * the string to string mapping in InchAopConfig
     * into
     * the method to advise mapping
     */
    public void parse() {
        //get the class name of the point class
        String targetClassName = targetClass.getName();
        /**
         *  get the <pointcutName,<adviseName,aspectMethodName>> mapping
         *   this map indicate the aspect method(aspectMethodName) that want cut in and where to cut in(adviseName) for this point cut
         */
        Map<String,Map<String,String>> pointCutAspectMethodMapping = config.getPointCutAspectMethodMapping();
        Class<?> aspectClass = null;
        //change the name corresponding mapping into name to method mapping
        try {
            //get aspect class eg: demo.aspect.logAspect
            aspectClass = Class.forName(config.getAspectClass());
            Map<String,Method> aspectMethods = new HashMap<String,Method>();
            //loop all method in aspectClass
            for(Method method : aspectClass.getMethods()) {
                //link full method name and the method it's self
                aspectMethods.put(config.getAspectClass() + "." + method.getName(),method);
            }
            //loop all method in the point cut class
            for(Method method : targetClass.getMethods()) {
                //get the full name of the method in pointcut class
                String methodName = targetClassName + "." + method.getName();
                //if the point cut method need cutting in
                if (pointCutAspectMethodMapping.containsKey(methodName)) {
                    List<Object> advices = new LinkedList<Object>();
                    Map<String,String> cutMethod = pointCutAspectMethodMapping.get(methodName);
                    //if the cut in position is before
                    if (cutMethod.containsKey(InchAopConfig.ASPECT_BEFORE)){
                        Object instance = aspectClass.getConstructor().newInstance();
                        Method aspectMethod = aspectMethods.get(cutMethod.get(InchAopConfig.ASPECT_BEFORE));
                        advices.add(new InchMethodBeforeAdvice(aspectMethod,instance));
                    }
                    //if the cut in position is after
                    if (cutMethod.containsKey(InchAopConfig.ASPECT_AFTER)){
                        Object instance = aspectClass.getConstructor().newInstance();
                        Method aspectMethod = aspectMethods.get(cutMethod.get(InchAopConfig.ASPECT_AFTER));
                        advices.add(new InchMethodAfterAdvice(aspectMethod,instance));
                    }
                    //if the cut in position is after point cut method returning
                    if (cutMethod.containsKey(InchAopConfig.ASPECT_AFTER_RETURNING)){
                        Object instance = aspectClass.getConstructor().newInstance();
                        Method aspectMethod = aspectMethods.get(cutMethod.get(InchAopConfig.ASPECT_AFTER_RETURNING));
                        advices.add(new InchMethodAfterReturningAdvice(aspectMethod,instance));
                    }
                    //if the cut in position is after exception throws
                    if (cutMethod.containsKey(InchAopConfig.ASPECT_AFTER_THROW)){
                        Object instance = aspectClass.getConstructor().newInstance();
                        Method aspectMethod = aspectMethods.get(cutMethod.get(InchAopConfig.ASPECT_AFTER_THROW));
                        advices.add(new InchMethodAfterThrowingAdvice(aspectMethod,instance));
                    }
                    /** around is not implemented  */
//                    if (cutMethod.containsKey(InchAopConfig.ASPECT_AROUND)){
//                        Object instance = aspectClass.getConstructor().newInstance();
//                        Method aspectMethod = aspectMethods.get(cutMethod.get(InchAopConfig.ASPECT_AROUND));
//                        advices.add(new InchMethodAroundAdvice(aspectMethod,instance));
//                    }
                    //link the point cut method with all advices
                    methodCache.put(method, advices);
                }
            }
    } catch (ClassNotFoundException | NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }

    /**
     * get all the advise need to be cut in(before/after/...) this point
     * eg: {InchMethodBeforeAdvice,InchMethodAfterMethod,..........}
     * @param method    the point cut method
     * @param targetClass   the targetclass of this method
     * @return  List of advise that should cut in
     * @throws Exception
     */
    public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, Class<?> targetClass) throws Exception {
        //all the
        List<Object> cached = methodCache.get(method);
        if(cached == null) {
            Method m = targetClass.getMethod(method.getName(),method.getParameterTypes());
            cached = methodCache.get(m);
            this.methodCache.put(m,cached);
        }
        return cached;
    }


    /**
     * check if the point cut is the method in the pointcut class
     * @return true for yes/false for no
     */
    public boolean pointCutMatch() {
        String targetClassName= this.targetClass.getName();
        for(String pointCut : pointCuts){
            if(pointCut.startsWith(targetClassName)){
                return true;
            }
        }
        return false;
    }
    public Class getTargetClass() {
        return targetClass;
    }

    public void setTargetClass(Class targetClass) {
        this.targetClass = targetClass;
    }

    public Object getTarget() {
        return target;
    }

    public void setTarget(Object target) {
        this.target = target;
    }
}
